Skip to content

feat: desktop WSL onboarding + happy experience#23407

Open
Hona wants to merge 108 commits intoanomalyco:devfrom
Hona:desktop-wsl-onboarding
Open

feat: desktop WSL onboarding + happy experience#23407
Hona wants to merge 108 commits intoanomalyco:devfrom
Hona:desktop-wsl-onboarding

Conversation

@Hona
Copy link
Copy Markdown
Member

@Hona Hona commented Apr 19, 2026

Issue for this PR

Closes #

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.

If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!

How did you verify your code works?

Screenshots / recordings

If this is a UI change, please include a screenshot or recording.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

If you do not follow this template your PR will be automatically rejected.

Hona and others added 30 commits April 10, 2026 10:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Electron-side WSL server onboarding and multi-distro support, replacing the old single on/off WSL toggle with discoverable WSL sidecars that can be added and selected like other servers. It also updates the app UI so WSL-backed sessions use WSL-aware path handling and server management flows.

Changes:

  • Replaces boolean WSL enablement with a new Electron WSL servers API, controller, IPC surface, and WSL process helpers.
  • Adds app-side WSL state/context plus new server-selection/onboarding UI for adding, updating, retrying, and displaying WSL servers.
  • Adjusts routing, picker behavior, and a few supporting utilities/refactors to fit WSL-backed local workflows.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/desktop/src/index.tsx Removes legacy Tauri-side WSL picker/config behavior.
packages/desktop-electron/src/renderer/index.tsx Wires Electron renderer platform to active WSL server state and WSL server list.
packages/desktop-electron/src/renderer/env.d.ts Adds activeServer to renderer window globals.
packages/desktop-electron/src/preload/types.ts Defines WSL server/runtime/state IPC types and expands wslPath.
packages/desktop-electron/src/preload/index.ts Exposes WSL server IPC methods in preload.
packages/desktop-electron/src/main/wsl.ts Adds WSL command helpers, probing, install, upgrade, and terminal utilities.
packages/desktop-electron/src/main/wsl-servers.ts Implements WSL server controller/state management and persistence.
packages/desktop-electron/src/main/server.ts Adds WSL sidecar spawn logic and shared port allocation.
packages/desktop-electron/src/main/ipc.ts Registers WSL server IPC handlers and routes relaunch through injected deps.
packages/desktop-electron/src/main/index.ts Boots/stops WSL servers from Electron main process and hooks new IPC deps.
packages/desktop-electron/src/main/constants.ts Replaces old WSL enabled store key with WSL servers store key.
packages/desktop-electron/src/main/apps.ts Makes WSL path conversion distro-aware and handles WSL UNC paths.
packages/app/src/utils/solid-dnd.tsx Simplifies drag constraint transformer registration.
packages/app/src/pages/layout.tsx Uses web picker for WSL sidecars and passes home-navigation callback to server dialog.
packages/app/src/pages/home.tsx Mirrors WSL-aware picker/server-dialog behavior on home page.
packages/app/src/index.ts Re-exports WSL context/types from app package surface.
packages/app/src/context/wsl-servers.tsx Adds query-backed WSL servers context/provider.
packages/app/src/context/server.tsx Tracks active server globally and treats sidecars as local servers.
packages/app/src/context/platform.tsx Extends platform contract with WSL server management types/API.
packages/app/src/context/global-sync/child-store.ts Extracts reusable child disposal helper and disposeAll.
packages/app/src/context/global-sync.tsx Uses new disposeAll cleanup path.
packages/app/src/components/status-popover.tsx Passes close handler into popover body.
packages/app/src/components/status-popover-body.tsx Updates server switching flow to close popover and coordinate route changes.
packages/app/src/components/server/server-row.tsx Displays WSL badge and supports explicit version override.
packages/app/src/components/dialog-wsl-server.tsx Adds multi-step WSL onboarding/install/update wizard UI.
packages/app/src/components/dialog-select-server.tsx Integrates WSL server add/manage/update/remove flows into server selection dialog.
packages/app/src/app.tsx Mounts WSL servers provider and adjusts keyed app/router rendering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +503 to +506
const handleAddedWsl = async (distro: string) => {
const key = ServerConnection.Key.make(`wsl:${distro}`)
setStore("addWsl", "showWizard", false)
const conn = items().find((item) => ServerConnection.key(item) === key)
</Show>

<Show when={i.type === "http"}>
<Show when={i.type === "http" || i.type === "sidecar"}>
})
const opencodeReady = createMemo(() => {
const check = opencodeCheck()
return !!check?.resolvedPath && !check.error
Comment on lines +335 to +339
<div class="rounded-md border border-border-weak-base px-3 py-3 flex items-center justify-between gap-3">
<div class="text-12-regular text-text-warning-base">Windows restart required.</div>
<Button variant="secondary" size="large" onClick={() => void platform.restart()}>
Relaunch OpenCode
</Button>
Comment on lines +343 to +344
if (distro && !/^[a-zA-Z0-9_.-]+$/.test(distro)) {
return Promise.reject(new Error("Invalid distro name"))
Comment on lines +295 to +309
async installOpencode(name: string) {
await runJob({ kind: "install-opencode", distro: name, startedAt: Date.now() }, async (abort) => {
const resolved = await resolveWslOpencode(name, { signal: abort.signal })
const existingVersion = resolved
? await readWslCommandVersion(resolved, name, { signal: abort.signal })
: null
const result =
resolved && existingVersion
? await upgradeWslOpencode(appVersion, resolved, name, { signal: abort.signal })
: await installWslOpencode(appVersion, name, { signal: abort.signal })
if (result.code !== 0) {
throw new Error(summarize(result.stderr || result.stdout) || "OpenCode installation failed")
}
await refreshOpencodeCheck(name, { signal: abort.signal })
})
Comment on lines +347 to +350
for (const item of wslServers.data?.servers ?? []) {
const runtime = item.runtime
if (runtime.kind !== "ready") continue
list.push({
Comment on lines +389 to +401
const navigateHome = () => props.onNavigateHome?.()

const apply = () => {
dialog.close()
if (persist && conn.type === "http") {
server.add(conn)
navigateHome()
return
}

batch(() => {
navigateHome()
server.setActive(nextKey)
Comment on lines +38 to +43
// joiner. If the requested distro differs from the UNC distro, we still
// translate literally — callers are responsible for only picking paths
// inside the active distro.
if (mode === "linux") {
const unc = parseWslUncPath(path)
if (unc) return `/${unc.subpath}`
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

createEffect(() => {
if (typeof window === "undefined") return
window.__OPENCODE__ ??= {}
window.__OPENCODE__.activeServer = state.active
Comment on lines +32 to +43
export async function wslPath(path: string, mode: "windows" | "linux" | null, distro?: string | null): Promise<string> {
if (process.platform !== "win32") return path

// `\\wsl$\<distro>\...` / `\\wsl.localhost\<distro>\...` -> `/<subpath>` in
// the target distro. Do the conversion in-process rather than shelling out
// to `wslpath -u`, which mangles backslashes via wsl.exe's command-line
// joiner. If the requested distro differs from the UNC distro, we still
// translate literally — callers are responsible for only picking paths
// inside the active distro.
if (mode === "linux") {
const unc = parseWslUncPath(path)
if (unc) return `/${unc.subpath}`
Comment on lines +343 to +345
if (distro && !/^[a-zA-Z0-9_.-]+$/.test(distro)) {
return Promise.reject(new Error("Invalid distro name"))
}
})
const opencodeReady = createMemo(() => {
const check = opencodeCheck()
return !!check?.resolvedPath && !check.error
Comment on lines +182 to +213
if (!state || state.job?.kind === "runtime") return "Checking WSL..."
if (state.pendingRestart) return "Windows needs a restart to finish installing WSL."
if (state.runtime?.available) return state.runtime.version ?? "WSL is ready."
return state.runtime?.error ?? "WSL is required to continue."
})

const distroMessage = createMemo(() => {
const state = current()
if (!state) return "Checking distros..."
const distro = store.selectedDistro
if (state.job?.kind === "install-distro") return `Installing ${state.job.distro}...`
if (state.job?.kind === "probe-distro") return `Checking ${state.job.distro}...`
if (state.job?.kind === "distros") return "Listing distros..."
if (distroUnavailableMessage()) return distroUnavailableMessage()!
if (selectedProbe() && distroReady()) return `${selectedProbe()!.name} is ready.`
if (distro) return `Finishing setup for ${distro}.`
return "Pick a distro or install one below."
})

const opencodeMessage = createMemo(() => {
const state = current()
if (!state) return "Checking OpenCode..."
const distro = store.selectedDistro
if (state.job?.kind === "probe-opencode" || state.job?.kind === "install-opencode") {
return distro ? `Checking OpenCode in ${distro}...` : "Checking OpenCode..."
}
if (opencodeCheck()?.error) return opencodeCheck()!.error
if (opencodeCheck()?.matchesDesktop === false) {
return distro ? `Update OpenCode in ${distro}.` : "Update OpenCode."
}
if (opencodeReady()) return distro ? `OpenCode is ready in ${distro}.` : "OpenCode is ready."
return distro ? `Install OpenCode in ${distro}.` : "Choose a distro first."
Comment on lines 252 to +257
aria-disabled={blocked()}
onClick={() => {
if (blocked()) return
props.close?.()
navigate("/")
queueMicrotask(() => server.setActive(key))
const activate = () => {
<span>{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}</span>
<span>
{isAddWslMode()
? "Add WSL server"
Comment on lines 728 to 739
@@ -579,35 +737,54 @@ export function DialogSelectServer() {
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1">
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants